﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Csla;
using Csla.Core;
using Csla.Reflection;
using Csla.Rules;


namespace FlyingColorsDesktop.Common
{
	/// <summary>
	/// Base class used to create ViewModel objects that
	/// implement their own commands/verbs/actions.
	/// </summary>
	/// <typeparam name="T">Type of the Model object.</typeparam>
	public class ViewModelBase<T> : INotifyPropertyChanged
	{
		/// <summary>
		/// Create new instance of base class used to create ViewModel objects that
		/// implement their own commands/verbs/actions.
		/// </summary>
		public ViewModelBase()
		{
			SetPropertiesAtObjectLevel();
		}

		#region Properties

		private T _model = default(T);
		/// <summary>
		/// Gets or sets the Model object.
		/// </summary>
		public T Model
		{
			get { return _model; }
			set
			{
				T oldModel = _model;
				_model = value;
				OnModelChanged(oldModel, _model);
				if (_manageObjectLifetime)
				{
					var undo = _model as ISupportUndo;
					if (undo != null)
					{
						undo.BeginEdit();
					}
				}
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether the
		/// ViewModel should automatically managed the
		/// lifetime of the Model.
		/// </summary>
		private bool _manageObjectLifetime = false;
		/// <summary>
		/// Gets or sets a value indicating whether the
		/// ViewManageObjectLifetime should automatically managed the
		/// lifetime of the ManageObjectLifetime.
		/// </summary>
		[Browsable(false)]
		[Display(AutoGenerateField = false)]
		public bool ManageObjectLifetime
		{
			get { return _manageObjectLifetime; }
			set { _manageObjectLifetime = value; }
		}

		private Exception _error;

		/// <summary>
		/// Gets the Error object corresponding to the
		/// last asyncronous operation.
		/// </summary>
		[Browsable(false)]
		[Display(AutoGenerateField = false)]
		public Exception Error
		{
			get { return _error; }
			protected set
			{
				if (!ReferenceEquals(_error, value))
				{
					_error = value;
					OnPropertyChanged("Error");
					if (_error != null)
					{
						OnError(_error);
					}
				}
			}
		}

		/// <summary>
		/// Event raised when an error occurs during processing.
		/// </summary>
		public event EventHandler<ErrorEventArgs> ErrorOccurred;

		/// <summary>
		/// Raises ErrorOccurred event when an error occurs
		/// during processing.
		/// </summary>
		/// <param name="error"></param>
		protected virtual void OnError(Exception error)
		{
			if (ErrorOccurred != null)
			{
				ErrorOccurred(this, new ErrorEventArgs { Error = error });
			}
		}

		private bool _isBusy;

		/// <summary>
		/// Gets a value indicating whether this object is
		/// executing an asynchronous process.
		/// </summary>
		public bool IsBusy
		{
			get { return _isBusy; }
			protected set
			{
				_isBusy = value;
				OnPropertyChanged("IsBusy");
				SetProperties();
			}
		}

		#endregion

		#region Can___ properties

		private bool _canSave = false;

		/// <summary>
		/// Gets a value indicating whether the Model
		/// can currently be saved.
		/// </summary>
		public bool CanSave
		{
			get
			{
				return _canSave;
			}
			private set
			{
				if (_canSave != value)
				{
					_canSave = value;
					OnPropertyChanged("CanSave");
				}
			}
		}

		private bool _canCancel = false;

		/// <summary>
		/// Gets a value indicating whether the Model
		/// can currently be canceled.
		/// </summary>
		public bool CanCancel
		{
			get
			{
				return _canCancel;
			}
			private set
			{
				if (_canCancel != value)
				{
					_canCancel = value;
					OnPropertyChanged("CanCancel");
				}
			}
		}

		private bool _canCreate = false;

		/// <summary>
		/// Gets a value indicating whether an instance
		/// of the Model
		/// can currently be created.
		/// </summary>
		public bool CanCreate
		{
			get
			{
				return _canCreate;
			}
			private set
			{
				if (_canCreate != value)
				{
					_canCreate = value;
					OnPropertyChanged("CanCreate");
				}
			}
		}

		private bool _canDelete = false;

		/// <summary>
		/// Gets a value indicating whether the Model
		/// can currently be deleted.
		/// </summary>
		public bool CanDelete
		{
			get
			{
				return _canDelete;
			}
			private set
			{
				if (_canDelete != value)
				{
					_canDelete = value;
					OnPropertyChanged("CanDelete");
				}
			}
		}

		private bool _canFetch = false;

		/// <summary>
		/// Gets a value indicating whether an instance
		/// of the Model
		/// can currently be retrieved.
		/// </summary>
		public bool CanFetch
		{
			get
			{
				return _canFetch;
			}
			private set
			{
				if (_canFetch != value)
				{
					_canFetch = value;
					OnPropertyChanged("CanFetch");
				}
			}
		}

		private bool _canRemove = false;

		/// <summary>
		/// Gets a value indicating whether the Model
		/// can currently be removed.
		/// </summary>
		public bool CanRemove
		{
			get
			{
				return _canRemove;
			}
			private set
			{
				if (_canRemove != value)
				{
					_canRemove = value;
					OnPropertyChanged("CanRemove");
				}
			}
		}

		private bool _canAddNew = false;

		/// <summary>
		/// Gets a value indicating whether the Model
		/// can currently be added.
		/// </summary>
		public bool CanAddNew
		{
			get
			{
				return _canAddNew;
			}
			private set
			{
				if (_canAddNew != value)
				{
					_canAddNew = value;
					OnPropertyChanged("CanAddNew");
				}
			}
		}

		private void SetProperties()
		{
			ITrackStatus targetObject = Model as ITrackStatus;
			ICollection list = Model as ICollection;
			INotifyBusy busyObject = Model as INotifyBusy;
			bool isObjectBusy = false;
			if (busyObject != null && busyObject.IsBusy)
				isObjectBusy = true;
			if (Model != null && targetObject != null)
			{

				if (CanEditObject && targetObject.IsSavable && !isObjectBusy)
					CanSave = true;
				else
					CanSave = false;

				if (CanEditObject && targetObject.IsDirty && !isObjectBusy)
					CanCancel = true;
				else
					CanCancel = false;

				if (CanCreateObject && !targetObject.IsDirty && !isObjectBusy)
					CanCreate = true;
				else
					CanCreate = false;

				if (CanDeleteObject && !isObjectBusy)
					CanDelete = true;
				else
					CanDelete = false;

				if (CanGetObject && !targetObject.IsDirty && !isObjectBusy)
					CanFetch = true;
				else
					CanFetch = false;

				if (list != null)
				{
					Type itemType = Utilities.GetChildItemType(Model.GetType());
					if (itemType != null)
					{

						if (BusinessRules.HasPermission(AuthorizationActions.DeleteObject, itemType) && list.Count > 0 && !isObjectBusy)
							CanRemove = true;
						else
							CanRemove = false;

						if (BusinessRules.HasPermission(AuthorizationActions.CreateObject, itemType) && !isObjectBusy)
							CanAddNew = true;
						else
							CanAddNew = false;
					}
					else
					{
						CanAddNew = false;
						CanRemove = false;
					}
				}
				else
				{
					CanRemove = false;
					CanAddNew = false;
				}
			}
			else if (list != null)
			{
				Type itemType = Utilities.GetChildItemType(Model.GetType());
				if (itemType != null)
				{

					if (BusinessRules.HasPermission(AuthorizationActions.DeleteObject, itemType) && list.Count > 0 && !isObjectBusy)
						CanRemove = true;
					else
						CanRemove = false;

					if (BusinessRules.HasPermission(AuthorizationActions.CreateObject, itemType) && !isObjectBusy)
						CanAddNew = true;
					else
						CanAddNew = false;
				}
				else
				{
					CanAddNew = false;
					CanRemove = false;
				}
			}
			else
			{
				CanCancel = false;
				CanCreate = false;
				CanDelete = false;
				CanFetch = !IsBusy;
				CanSave = false;
				CanRemove = false;
				CanAddNew = false;
			}
		}

		#endregion

		#region Can methods that only account for user rights

		private bool _canCreateObject;

		/// <summary>
		/// Gets a value indicating whether the current
		/// user is authorized to create a Model.
		/// </summary>
		public bool CanCreateObject
		{
			get { return _canCreateObject; }
			protected set
			{
				if (_canCreateObject != value)
				{
					_canCreateObject = value;
					OnPropertyChanged("CanCreateObject");
				}
			}
		}

		private bool _canGetObject;

		/// <summary>
		/// Gets a value indicating whether the current
		/// user is authorized to retrieve a Model.
		/// </summary>
		public bool CanGetObject
		{
			get { return _canGetObject; }
			protected set
			{
				if (_canGetObject != value)
				{
					_canGetObject = value;
					OnPropertyChanged("CanGetObject");
				}
			}
		}

		private bool _canEditObject;

		/// <summary>
		/// Gets a value indicating whether the current
		/// user is authorized to save (insert or update
		/// a Model.
		/// </summary>
		public bool CanEditObject
		{
			get { return _canEditObject; }
			protected set
			{
				if (_canEditObject != value)
				{
					_canEditObject = value;
					OnPropertyChanged("CanEditObject");
				}
			}
		}

		private bool _canDeleteObject;

		/// <summary>
		/// Gets a value indicating whether the current
		/// user is authorized to delete
		/// a Model.
		/// </summary>
		public bool CanDeleteObject
		{
			get { return _canDeleteObject; }
			protected set
			{
				if (_canDeleteObject != value)
				{
					_canDeleteObject = value;
					OnPropertyChanged("CanDeleteObject");
				}
			}
		}

		private void SetPropertiesAtObjectLevel()
		{
			Type sourceType = typeof(T);
			CanCreateObject = BusinessRules.HasPermission(AuthorizationActions.CreateObject, sourceType);
			CanGetObject = BusinessRules.HasPermission(AuthorizationActions.GetObject, sourceType);
			CanEditObject = BusinessRules.HasPermission(AuthorizationActions.EditObject, sourceType);
			CanDeleteObject = BusinessRules.HasPermission(AuthorizationActions.DeleteObject, sourceType);
		}

		#endregion

		#region Verbs

#if !SILVERLIGHT
		/// <summary>
		/// Creates or retrieves a new instance of the 
		/// Model by invoking a static factory method.
		/// </summary>
		/// <param name="factoryMethod">Static factory method function.</param>
		/// <example>DoRefresh(() => BusinessList.GetList())</example>
		/// <example>DoRefresh(() => BusinessList.GetList(id))</example>
		protected void DoRefresh(Func<T> factoryMethod)
		{
			if (typeof(T) != null)
			{
				Error = null;
				try
				{
					Model = factoryMethod.Invoke();
				}
				catch (Exception ex)
				{
					Error = ex;
				}
				OnRefreshed();
			}
		}

		/// <summary>
		/// Creates or retrieves a new instance of the 
		/// Model by invoking a static factory method.
		/// </summary>
		/// <param name="factoryMethod">Name of the static factory method.</param>
		/// <param name="factoryParameters">Factory method parameters.</param>
		protected virtual void DoRefresh(string factoryMethod, params object[] factoryParameters)
		{
			if (typeof(T) != null)
			{
				Error = null;
				try
				{
					Model = (T)MethodCaller.CallFactoryMethod(typeof(T), factoryMethod, factoryParameters);
				}
				catch (Exception ex)
				{
					Error = ex;
				}
				OnRefreshed();
			}
		}

		/// <summary>
		/// Creates or retrieves a new instance of the 
		/// Model by invoking a static factory method.
		/// </summary>
		/// <param name="factoryMethod">Name of the static factory method.</param>
		protected virtual void DoRefresh(string factoryMethod)
		{
			DoRefresh(factoryMethod, new object[] { });
		}
#endif

		/// <summary>
		/// Creates or retrieves a new instance of the 
		/// Model by invoking a static factory method.
		/// </summary>
		/// <param name="factoryMethod">Static factory method action.</param>
		/// <example>BeginRefresh(handler => BusinessList.BeginGetList(handler))</example>
		/// <example>BeginRefresh(handler => BusinessList.BeginGetList(id, handler))</example>
		protected void BeginRefresh(Action<EventHandler<DataPortalResult<T>>> factoryMethod)
		{
			if (typeof(T) != null)
				try
				{
					Error = null;
					IsBusy = true;

					var handler = (EventHandler<DataPortalResult<T>>)CreateHandler(typeof(T));
					factoryMethod(handler);
				}
				catch (Exception ex)
				{
					Error = ex;
					IsBusy = false;
				}
		}

		/// <summary>
		/// Creates or retrieves a new instance of the 
		/// Model by invoking a static factory method.
		/// </summary>
		/// <param name="factoryMethod">Name of the static factory method.</param>
		/// <param name="factoryParameters">Factory method parameters.</param>
		protected virtual void BeginRefresh(string factoryMethod, params object[] factoryParameters)
		{
			if (typeof(T) != null)
				try
				{
					Error = null;
					IsBusy = true;
					var parameters = new List<object>(factoryParameters);
					parameters.Add(CreateHandler(typeof(T)));

					MethodCaller.CallFactoryMethod(typeof(T), factoryMethod, parameters.ToArray());
				}
				catch (Exception ex)
				{
					Error = ex;
					IsBusy = false;
				}
		}

		/// <summary>
		/// Creates or retrieves a new instance of the 
		/// Model by invoking a static factory method.
		/// </summary>
		/// <param name="factoryMethod">Name of the static factory method.</param>
		protected virtual void BeginRefresh(string factoryMethod)
		{
			BeginRefresh(factoryMethod, new object[] { });
		}

		private Delegate CreateHandler(Type objectType)
		{
			var args = typeof(DataPortalResult<>).MakeGenericType(objectType);
			System.Reflection.MethodInfo method = MethodCaller.GetNonPublicMethod(GetType(), "QueryCompleted");
			Delegate handler = Delegate.CreateDelegate(typeof(EventHandler<>).MakeGenericType(args), this, method);
			return handler;
		}

		private void QueryCompleted(object sender, EventArgs e)
		{
			try
			{
				var eventArgs = (IDataPortalResult)e;
				if (eventArgs.Error == null)
				{
					var model = (T)eventArgs.Object;
					OnRefreshing(model);
					Model = model;
				}
				else
					Error = eventArgs.Error;
				OnRefreshed();
			}
			finally
			{
				IsBusy = false;
			}
		}


		/// <summary>
		/// Method called after a refresh operation 
		/// has completed and before the model is updated 
		/// (when successful).
		/// </summary>
		/// <param name="model">The model.</param>
		protected virtual void OnRefreshing(T model)
		{ }

		/// <summary>
		/// Method called after a refresh operation 
		/// has completed (whether successful or
		/// not).
		/// </summary>
		protected virtual void OnRefreshed()
		{ }

#if !SILVERLIGHT
		/// <summary>
		/// Saves the Model, first committing changes
		/// if ManagedObjectLifetime is true.
		/// </summary>
		protected virtual T DoSave()
		{
			T result = (T)Model;
			Error = null;
			try
			{
				var savable = Model as Csla.Core.ISavable;
				if (ManageObjectLifetime)
				{
					// clone the object if possible
					ICloneable clonable = Model as ICloneable;
					if (clonable != null)
						savable = (Csla.Core.ISavable)clonable.Clone();

					//apply changes
					var undoable = savable as Csla.Core.ISupportUndo;
					if (undoable != null)
						undoable.ApplyEdit();
				}

				result = (T)savable.Save();

				Model = result;
				OnSaved();
			}
			catch (Exception ex)
			{
				Error = ex;
				OnSaved();
			}
			return result;
		}
#endif

		/// <summary>
		/// Saves the Model, first committing changes
		/// if ManagedObjectLifetime is true.
		/// </summary>
		protected virtual void BeginSave()
		{
			try
			{
				var savable = Model as Csla.Core.ISavable;
				if (ManageObjectLifetime)
				{
					// clone the object if possible
					ICloneable clonable = Model as ICloneable;
					if (clonable != null)
						savable = (Csla.Core.ISavable)clonable.Clone();

					//apply changes
					var undoable = savable as Csla.Core.ISupportUndo;
					if (undoable != null)
						undoable.ApplyEdit();
				}

				savable.Saved += (o, e) =>
				{
					IsBusy = false;
					if (e.Error == null)
					{
						var result = e.NewObject;
						var model = (T)result;
						OnSaving(model);
						Model = model;
					}
					else
					{
						Error = e.Error;
					}
					OnSaved();
				};
				Error = null;
				IsBusy = true;
				savable.BeginSave();
			}
			catch (Exception ex)
			{
				IsBusy = false;
				Error = ex;
				OnSaved();
			}
		}

		/// <summary>
		/// Method called after a save operation 
		/// has completed and before Model is updated 
		/// (when successful).
		/// </summary>
		protected virtual void OnSaving(T model)
		{ }

		/// <summary>
		/// Method called after a save operation 
		/// has completed (whether successful or
		/// not).
		/// </summary>
		protected virtual void OnSaved()
		{ }

		/// <summary>
		/// Cancels changes made to the model 
		/// if ManagedObjectLifetime is true.
		/// </summary>
		protected virtual void DoCancel()
		{
			if (ManageObjectLifetime)
			{
				var undo = Model as Csla.Core.ISupportUndo;
				if (undo != null)
				{
					undo.CancelEdit();
					undo.BeginEdit();
				}
			}
		}

#if SILVERLIGHT
    /// <summary>
    /// Adds a new item to the Model (if it
    /// is a collection).
    /// </summary>
    protected virtual void BeginAddNew()
    {
      ((IBindingList)Model).AddNew();
      SetProperties();
    }
#else
		/// <summary>
		/// Adds a new item to the Model (if it
		/// is a collection).
		/// </summary>
		protected virtual object DoAddNew()
		{
			var ibl = ((IBindingList)Model);
			var result = ibl.AddNew();
			SetProperties();
			return result;
		}
#endif

		/// <summary>
		/// Removes an item from the Model (if it
		/// is a collection).
		/// </summary>
		protected virtual void DoRemove(object item)
		{
			((System.Collections.IList)Model).Remove(item);
			SetProperties();
		}

		/// <summary>
		/// Marks the Model for deletion (if it is an
		/// editable root object).
		/// </summary>
		protected virtual void DoDelete()
		{
			((Csla.Core.IEditableBusinessObject)Model).Delete();
		}

		#endregion

		#region Model Changes Handling

		/// <summary>
		/// Invoked when the Model changes, allowing
		/// event handlers to be unhooked from the old
		/// object and hooked on the new object.
		/// </summary>
		/// <param name="oldValue">Previous Model reference.</param>
		/// <param name="newValue">New Model reference.</param>
		protected virtual void OnModelChanged(T oldValue, T newValue)
		{
			if (ReferenceEquals(oldValue, newValue)) return;

			// unhook events from old value
			if (oldValue != null)
			{
				var npc = oldValue as INotifyPropertyChanged;
				if (npc != null)
				{
					npc.PropertyChanged -= Model_PropertyChanged;
				}
				var ncc = oldValue as INotifyChildChanged;
				if (ncc != null)
				{
					ncc.ChildChanged -= Model_ChildChanged;
				}
				var nb = oldValue as INotifyBusy;
				if (nb != null)
				{
					nb.BusyChanged -= Model_BusyChanged;
				}
				var cc = oldValue as INotifyCollectionChanged;
				if (cc != null)
				{
					cc.CollectionChanged -= Model_CollectionChanged;
				}
			}

			// hook events on new value
			if (newValue != null)
			{
				var npc = newValue as INotifyPropertyChanged;
				if (npc != null)
				{
					npc.PropertyChanged += Model_PropertyChanged;
				}
				var ncc = newValue as INotifyChildChanged;
				if (ncc != null)
				{
					ncc.ChildChanged += Model_ChildChanged;
				}
				var nb = newValue as INotifyBusy;
				if (nb != null)
				{
					nb.BusyChanged += Model_BusyChanged;
				}
				var cc = newValue as INotifyCollectionChanged;
				if (cc != null)
				{
					cc.CollectionChanged += Model_CollectionChanged;
				}
			}

			SetProperties();
		}


		private void Model_BusyChanged(object sender, BusyChangedEventArgs e)
		{
			// only set busy state for entire object.  Ignore busy state based
			// on asynch rules being active
			if (e.PropertyName == string.Empty)
				IsBusy = e.Busy;
			else
				SetProperties();
		}


		private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			SetProperties();
		}

		private void Model_ChildChanged(object sender, ChildChangedEventArgs e)
		{
			SetProperties();
		}

		private void Model_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			SetProperties();
		}

		#endregion

		#region INotifyPropertyChanged

		public event PropertyChangedEventHandler PropertyChanged;

		/// <summary>
		/// Raise the PropertyChanged event.
		/// </summary>
		/// <param name="propertyName">Name of the changed property.</param>
		protected virtual void OnPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		#endregion
	}

	/// <summary>
	/// Contains information about the error that
	/// has occurred.
	/// </summary>
	public class ErrorEventArgs : EventArgs
	{
		/// <summary>
		/// Gets the Exception object for the error
		/// that occurred.
		/// </summary>
		public Exception Error { get; internal set; }
	}
}
